6.9 Erweiterung der Klassenhierarchie »CircleApplication«
 
Wir wollen uns nun noch einmal dem von uns immer weiter entwickelten Beispielprojekt CircleApplication zuwenden, den Entwurf um zwei weitere Klassen, nämlich um Rectangle und GraphicRectangle ergänzen und uns dabei die in diesem Kapitel gewonnenen Kenntnisse zunutze machen. Die Klasse Rectangle soll ein Rechteck beschreiben und die Klasse GraphicRectangle eine Operation bereitstellen, um ein Rectangle-Objekt in einer grafikfähigen Komponente darzustellen – analog zur Klasse GraphicCircle.
Ebenso wie ein Circle-Objekt soll auch ein Rectangle-Objekt seine Lage beschreiben. Dazu wird ein Punkt vom Typ Point mit seinen x- und y-Koordinaten definiert. Um bei der üblichen Konvention grafischer Benutzeroberflächen zu bleiben, soll es sich dabei um den oberen linken Punkt des Rechtecks handeln. Die Größe eines Rechtecks wird durch seine Breite und Länge definiert. Außerdem sind Methoden vorzusehen, um Umfang und Fläche zu berechnen und zwei Rectangle-Objekte zu vergleichen.
Die Klassendefinition Rectangle könnte dann wie folgt aussehen:
| // ---------- Delegate ----------
|
| public delegate void MeasureErrorRectangleEventHandler(Rectangle rect);
|
| public class Rectangle : IDisposable {
|
| // ---------- Ereignisse ----------
|
| public event MeasureErrorEventHandler MeasureError;
|
| // ---------- statische Felder ----------
|
| protected static int countRectangles;
|
| // ---------- Felder ----------
|
| protected Point position = new Point();
|
| protected double breite = 0;
|
| protected double laenge = 0;
|
| private bool counterReduced = false;
|
| // ---------- Konstruktoren ----------
|
| public Rectangle() {
|
| countRectangles++;
|
| }
|
| public Rectangle(double Laenge, double Breite) : this() {
|
| this.breite = Breite;
|
| this.laenge = Laenge;
|
| }
|
| public Rectangle(int X, int Y, double Laenge, double Breite)
|
| : this(Laenge, Breite) {
|
| this.position.X = X;
|
| this.position.Y = Y;
|
| this.Breite = Breite;
|
| this.Laenge = Laenge;
|
| }
|
| public Rectangle(double Laenge, double Breite, Point pt)
|
| : this(Laenge, Breite) {
|
| this.position = pt;
|
| }
|
| // ---------- Destruktor ----------
|
| ~Rectangle() {
|
| if(!counterReduced)
|
| countRectangles--;
|
| }
|
| // Dispose aus der IDisposable-Schnittstelle
|
| public void Dispose() {
|
| if(!counterReduced) {
|
| countRectangles--;
|
| counterReduced = true;
|
| }
|
| }
|
| // ---------- Eigenschaften ----------
|
| public double Breite {
|
| get {return breite;}
|
| set {
|
| if(value >= 0)
|
| this.breite = value;
|
| else
|
| if(MeasureError != null)
|
| MeasureError(this);
|
| }
|
| }
|
| public double Laenge {
|
| get {return laenge;}
|
| set {
|
| if(value >= 0)
|
| this.laenge = value;
|
| else
|
| if(MeasureError != null)
|
| MeasureError(this);
|
| }
|
| }
|
| public Point Position {
|
| get {return position;}
|
| set {this.position = value;}
|
| }
|
| // ---------- Instanzmethoden ----------
|
| public double GetFlaeche() {
|
| return this.laenge * this.breite;
|
| }
|
| public double GetUmfang() {
|
| return 2 * (this.laenge + this.breite);
|
| }
|
| public Rectangle Bigger(Rectangle rect) {
|
| if(this.laenge * this.breite >= rect.laenge * rect.breite)
|
| return this;
|
| return rect;
|
| }
|
| public Rectangle Bigger(Rectangle rect, out bool equal) {
|
| equal = false;
|
| if(this.laenge * this.breite == rect.laenge * rect.breite)
|
| equal = true;
|
| return this.Bigger(rect);
|
| }
|
| public void MoveXY(Point newCenterPoint) {
|
| this.position = newCenterPoint;
|
| }
|
| // ---------- Klassenmethoden -----------
|
| public static double GetFlaeche(double Laenge, double Breite) {
|
| return Laenge * Breite;
|
| }
|
| public static double GetUmfang(double Laenge, double Breite) {
|
| return 2 * (Laenge + Breite);
|
| }
|
| public static Rectangle Bigger(Rectangle rect1,
|
| Rectangle rect2) {
|
| if(rect1.breite * rect1.laenge >= rect2.laenge * rect2.breite)
|
| return rect1;
|
| return rect2;
|
| }
|
| public static Rectangle Bigger(Rectangle rect1,
|
| Rectangle rect2, out bool equal) {
|
| equal = false;
|
| if(rect1.breite * rect1.laenge == rect2.laenge * rect2.breite)
|
| equal = true;
|
| return Rectangle.Bigger(rect1, rect2);
|
| }
|
| public static bool IsBigger(Rectangle rect1, Rectangle rect2) {
|
| if(rect1.breite * rect1.laenge >= rect2.laenge * rect2.breite)
|
| return true;
|
| return false;
|
| }
|
| // ---------- Klasseneigenschaften ----------
|
| public static int CountRectangles {
|
| get {return countRectangles;}
|
| }
|
| }
|
Und jetzt noch die Klasse GraphicCircle:
| public class GraphicRectangle : Rectangle {
|
| public GraphicRectangle() : base() { }
|
| public GraphicRectangle(double Laenge, double Breite)
|
| : base(Laenge, Breite) { }
|
| public GraphicRectangle(int X, int Y, double Laenge, double Breite)
|
| :base(X, Y, Laenge, Breite) { }
|
| public GraphicRectangle(double Laenge, double Breite, Point pt)
|
| : base(Laenge, Breite,pt) { }
|
| // objektspezifische Methode
|
| public void Draw() {
|
| // Anweisungen
|
| Console.WriteLine("Das Rechteck wird gezeichnet");
|
| }
|
| }
|
Es ist deutlich zu erkennen, dass sich die Klassen Rectangle und Circle ähneln, ebenso die beiden Klassen GraphicCircle und GraphicRectangle. Dies spricht dafür, allen vier Klassen eine Basisklasse vorzuschalten, welche die gemeinsamen Merkmale eines Kreises und eines Rechtecks beschreibt: Wir werden diese Klasse im Folgenden GeometricObject nennen.
Ein weiteres Argument für diese Lösung ist die sich daraus ergebende Gleichnamigkeit der gemeinsamen Merkmale: Es werden dann die Methoden, die ihren Fähigkeiten nach Gleiches leisten, unabhängig vom Typ des zugrunde liegenden Objekts in gleicher Weise aufgerufen. Einerseits lässt sich dadurch die abstrahierte Artverwandtschaft der beiden geometrischen Objekte Kreis und Rechteck verdeutlichen, andererseits wird die Benutzung der Klassen wesentlich vereinfacht, da nicht zwei unterschiedliche Methodennamen dasselbe Leistungsmerkmal beschreiben.
Nach diesen ersten Überlegungen soll nun die Klasse GeometricObject implementiert werden.
 Hier klicken, um das Bild zu vergrößern
Abbildung 6.6 Die Klassenhierarchie des Projekts »CircleApplication«
6.9.1 Die Klasse »GeometricObject«
 
Vergleichen wir jetzt Schritt für Schritt die einzelnen Klassenmitglieder von Circle und Rectangle, um daraus ein einheitliches Konzept für den Entwurf des Oberbegriffs GeometricObject zu formulieren.
Instanzvariablen und Eigenschaftsmethoden
Ein Circle-Objekt wird nach dem augenblicklichen Stand durch seinen Radius und ein Point-Objekt namens center beschrieben, dessen Koordinaten über die Eigenschaftsmethoden XKoordinate und YKoordinate festgelegt werden.
Auch zur Positionsbeschreibung eines Rechtecks dient ein Point-Objekt. Allerdings kann nach der augenblicklichen Implementierung die Positionierung nur über die direkte Übergabe eines Point-Objekts an die Eigenschaft Position erfolgen.
Obwohl sich beide Klassendefinitionen hinsichtlich der Positionsdatenübergabe unterscheiden, bietet es sich uns an, jeden der beiden Typen sowohl mit einer Eigenschaft vom Typ Point als auch mit Eigenschaften, die das Festlegen der Koordinaten in x- und in y-Richtung ermöglichen, auszustatten. Mit dieser ergänzenden Maßnahme erhöhen wir für jeden Typ sogar noch die Flexibilität, ohne dabei Einbußen in Kauf nehmen zu müssen. Wir können also alle mit der Positionierung im Zusammenhang stehenden Klassenelemente in die Basisklasse GeometricObject auslagern.
Der Radius eines Circle-Objekts sowie die Länge und die Breite eines Rectangle-Objekts sind objektspezifische Daten. Diese Instanzvariablen lassen wir in den ursprünglichen Klassendefinitionen.
Beide Klassendefinitionen definieren außerdem das private Feld counterReduced, mit dem der Objektzähler beim Zerstören des Objekts mit Dispose und dem Destruktor gesteuert wird. Unter Änderung des Zugriffsmodifizierers in protected ist auch dieses Datenmember ein erstklassiger Kandidat für die neu geschaffene Basisklasse.
| // ---------- Instanzvariablen in GeometricObject ----------
|
| protected Point position = new Point();
|
| protected bool counterReduced = false;
|
| // ---------- Eigenschaftsmethoden ----------
|
| public int XKoordinate {
|
| get {return this.position.X;}
|
| set {this.position.X = value;}
|
| }
|
| public int YKoordinate {
|
| get {return this.position.Y;}
|
| set {this.position.Y = value;}
|
| }
|
| public Point Position {
|
| get {return position;}
|
| set {this.position = value;}
|
| }
|
Konstruktoren und Destruktoren
Da sich Konstruktoren und Destruktoren nicht an die abgeleiteten Klassen vererben, bleiben die Erstellungs- und Zerstörungsroutinen in Circle und Rectangle unverändert. Ein eigener Konstruktor in GeometricObject ist nicht notwendig, weder der parameterlose noch ein parametrisierter.
Die Instanzmethoden
Ein Vergleich hinsichtlich der Instanzmethoden beider Klassen führt zu der Erkenntnis, dass beide Klassen eine gleichnamige überladene Methode Bigger veröffentlichen, die zwei Objekte miteinander vergleicht und die Referenz auf das größere der verglichenen Objekte als Resultat des Methodenaufrufs an den Benutzer zurückgibt. Aus logischer Sicht erbringen diese Methoden sowohl in Circle als auch in Rectangle dieselbe Leistung, unterscheiden sich nur im Typ des Parameters bzw. des Rückgabewerts: Die Bigger-Methode in der Circle-Klasse nimmt die Referenz auf ein Kreisobjekt entgegen, die Klasse Rectangle die Referenz auf ein Rectangle-Objekt.
Wir können uns den Umstand zunutze machen, dass sowohl die Circle- als auch die Rectangle-Klasse aus derselben Basisklasse abgeleitet werden, und müssen dazu nur den Typ des Parameters und der Rückgabe entsprechend anpassen:
| // ----- Methoden in der Klasse GeometricObject -----
|
| public GeometricObject Bigger(GeometricObject geoObj) {
|
| if(this.GetFlaeche() >= geoObj.GetFlaeche())
|
| return this;
|
| return geoObj;
|
| }
|
| public GeometricObject Bigger(GeometricObject geoObj, out bool equal) {
|
| equal = false;
|
| if(this.GetFlaeche() == geoObj.GetFlaeche())
|
| equal = true;
|
| return this.Bigger(geoObj);
|
| }
|
Weil Circle und Rectangle von GeometricObject abgeleitet werden, gilt, dass sowohl ein Circle-Objekt als auch Rectangle-Objekt gleichzeitig ein Objekt vom Typ GeometricObject ist. Beide Methoden werden unter Übergabe einer spezifischen Objektreferenz aufgerufen, welche die Laufzeitumgebung implizit konvertiert. Als Nebeneffekt beschert uns diese Verallgemeinerung, dass wir nun in der Lage sind, die Flächen von zwei verschiedenen Typen zu vergleichen, denn nun kann die Bigger-Methode auf eine Circle-Referenz aufgerufen und als Argument die Referenz auf ein Rectangle-Objekt übergeben werden.
In der Bigger-Methode wird GetFlaeche aufgerufen, die natürlich weiterhin typgebunden ist, da die Fläche auf die typspezifischen Daten Radius bzw. Laenge und Breite zugreift. Es liegt aber nahe, GetFlaeche als abstrakte Methode festzulegen, die von den ableitenden Klassen Circle und Rectangle überschrieben werden muss. Polymorph wird dann der Zugriff auf die richtige Implementierung sichergestellt. Mit derselben Argumentation kann auch GetUmfang als abstrakte Methode in GeometricObject definiert werden.
| // ---------- abstrakte Instanzmethoden ---------------
|
| public abstract double GetFlaeche();
|
| public abstract double GetUmfang();
|
MoveXY ist im Vergleich zu den beiden vorgenannten Methoden typunabhängig und kann daher vollständig in GeometricObject implementiert werden.
| public void MoveXY(Point pt) {
|
| this.position = pt;
|
| }
|
Die Klassenmethoden
Die Argumentation, die uns dazu brachte, die Instanzmethode Bigger in der Basisklasse zu codieren, kann auch bei den Klassenmethoden Bigger und IsBigger geführt werden. Wir müssen jeweils nur den Typ des Parameters und bei der Methode Bigger auch den des Rückgabewerts ändern, um die Klassenmethoden in der Klasse GeometricObject bereitzustellen.
| // ---------- Klassenmethoden in GeometricObject -----------
|
| public static GeometricObject Bigger(GeometricObject geoObj1,
|
| GeometricObject geoObj2) {
|
| if(geoObj1.GetFlaeche() >= geoObj2.GetFlaeche())
|
| return geoObj1;
|
| return geoObj2;
|
| }
|
| public static GeometricObject Bigger(GeometricObject geoObj1,
|
| GeometricObject geoObj2, ref bool equal) {
|
| equal = false;
|
| if(geoObj1.GetFlaeche() == geoObj2.GetFlaeche())
|
| equal = true;
|
| return GeometricObject.Bigger(geoObj1, geoObj2);
|
| }
|
| public static bool IsBigger(GeometricObject geoObj1,
|
| GeometricObject geoObj2) {
|
| if(geoObj1.GetFlaeche() >= geoObj2.GetFlaeche())
|
| return true;
|
| return false;
|
| }
|
Das Ereignis »MeasureError«
Wir wollen nun auch das Ereignis MeasureError in GeometricObject implementieren und es allen abgeleiteten Klassen bereitstellen. Das ist jedoch nicht ganz so einfach und verlangt eine genauere Analyse, wie Sie noch sehen werden.
Rufen wir uns zuerst die vollständige Implementierung des Ereignisses in der Klasse Circle in Erinnerung:
| public delegate void MeasureErrorEventHandler(Circle c);
|
| ...
|
| public event MeasureErrorEventHandler MeasureError;
|
Ausgelöst wird das Ereignis MeasureError in der Eigenschaftsmethode Radius mit:
| public double Radius {
|
| get{return radius;}
|
| set {
|
| if(value >= 0)
|
| radius = value;
|
| else
|
| if(MeasureError != null)
|
| MeasureError(this);
|
| }
|
| }
|
Der Übergabeparameter des Delegaten vom Typ Circle versetzt den Benutzer der Klasse in die Lage, bei einer unzulässigen Zuweisung an Radius im Ereignishandler auf die Objektreferenz den Radius neu einzugeben, beispielsweise:
| public void SetNewRadius(Circle sender) {
|
| Console.WriteLine("Unzulässige Eingabe des Radius.");
|
| Console.Write("Neueingabe: ");
|
| sender.Radius = Convert.ToDouble(Console.ReadLine());
|
| }
|
Ähnlich ist der Sachverhalt in Rectangle. Diese Klasse stellt auch ein Ereignis MeasureError bereit, dieses ist jedoch vom Typ MeasureErrorRectangleEventHandler.
| public delegate void MeasureErrorRectangleEventHandler(Rectangle rect)
|
| ...
|
| public event MeasureErrorRectangleEventHandler MeasureError;
|
Das Ereignis wird, ähnlich wie in der Klasse Circle, ausgelöst, wenn einer der beiden Seitenlängen ein negativer Wert zugewiesen wird. Stellvertretend sei an dieser Stelle die Ereignismethode Breite gezeigt:
| public double Breite {
|
| get {return breite;}
|
| set {
|
| if(value >= 0)
|
| this.breite = value;
|
| else
|
| if(MeasureError != null)
|
| MeasureError(this);
|
| }
|
| }
|
MeasureError soll in der Klasse GeometricObject als Ereignis dienen, wenn entweder einem Circle- oder einem Rectangle-Objekt ein unzulässiger Wert übergeben wird. Damit wird sofort die erste Konsequenz klar: Da wir weiterhin davon ausgehen, dass bei Ereignisauslösung in einem Circle-Objekt dieses die Referenz auf sich selbst an den Ereignishandler übergeben will, müssen wir den Typ in der Parameterliste des Delegaten entsprechend anpassen. Ein Rectangle-Objekt, dessen Ereignis bisher noch nicht die Referenz auf sich selbst dem Ereignishandler mitteilte, kann davon nur profitieren.
| // ---------- vorläufiger Delegat in GeometricObject -----------
|
| public delegate void MeasureErrorEventHandler(GeometricObject geo);
|
Wir lassen zunächst alle anderen Aspekte hinsichtlich der Implementierung in den Klassen außen vor und stellen weitere Überlegungen an. Wird MeasureError bei dieser Definition in einem Circle-Objekt ausgelöst, kann im Ereignishandler der Radius nach vorheriger Konvertierung der Referenz sender neu zugewiesen werden, z.B.:
| public void SetNewMeasure(GeometricObject sender) {
|
| ...
|
| ((Circle)sender).Radius = Convert.ToDouble(Console.ReadLine());
|
| }
|
Etwas anders verhält es sich jedoch, wenn entweder die Breite oder die Länge eines Rechtecks nicht den Vorgaben entspricht. Woher soll der Ereignishandler die Information nehmen, welcher Eigenschaft ein unzulässiger, negativer Wert zugewiesen worden ist? Es stehen keinerlei Informationen darüber zur Verfügung, ob es sich um Breite oder Laenge gehandelt hat.
Wir sollten eine Lösung für dieses Problem finden, denn dasselbe Ereignis bei der Auslösung durch ein Circle-Objekt anders zu behandeln als bei der Auslösung durch ein Rectangle-Objekt wäre eine schlechte Alternative. Die Lösung muss für beide in Frage kommenden Typen zu einer gleichwertigen Behandlung führen.
Der Ansatz, der uns zum Ziel führt, geht über die Definition eines weiteren Parameters, der im Ereignishandler die Referenz auf den Wert liefert, der fälschlicherweise zugewiesen wurde.
| public delegate void MeasureErrorEventHandler(
|
| GeometricObject geo, ref double measure);
|
Der Ereignishandler in einer Clientklasse, der die Korrektur durch den Anwender zur Laufzeit zulässt, könnte im einfachsten Fall wie folgt codiert werden:
| // Ereignishandler in der Clientklasse
|
| public static void SetNewMeasure(GeometricObject sender, out double measure) {
|
| measure = Convert.ToDouble(Console.ReadLine());
|
| }
|
Sollte es sich als notwendig erweisen, kann die Referenz sender daraufhin untersucht werden, ob der Auslöser ein Circle- oder ein Rectangle-Objekt war. Aber auch ohne diese Unterscheidung wird der Referenzparameter den neuen Wert immer richtig an die Ereignisquelle weiterleiten.
Wir haben das endgültige Ziel nahezu erreicht, müssen aber jetzt noch dem Umstand Beachtung schenken, dass Ereignisse in der Klasse ausgelöst werden, in der sie definiert sind. Damit das in einer Basisklasse definierte Ereignis auch für den Code in einer abgeleiteten Klasse zugänglich ist, muss die Basisklasse GeometricObject nicht nur das Ereignis MeasureError, sondern auch eine Methode bereitstellen, die in der Art eines Adapters die Verbindung zwischen einer Sub- und der Basisklasse sicherstellt, wenn die abgeleitete Klasse das Ereignis auslöst:
| public void OnMeasureError(GeometricObject geo, ref double measure) {
|
| if(MeasureError != null)
|
| MeasureError(geo, ref measure);
|
| }
|
Objekte der abgeleiteten Klassen Circle und Rectangle müssen jetzt die Methode OnMeasureError aufrufen und neben dem this-Zeiger auch den Wert als Referenz übergeben, der die Ablehnung im Objekt verursacht hat. Behandelt der Client das Ereignis und weist einen neuen Wert zu, wird über OnMeasureError der korrigierte Wert an das Objekt der abgeleiteten Klasse weitergeleitet. Hier kann nun eine erneute Überprüfung durch einen weitere Aufruf der entsprechenden Ereignismethode erfolgen. Beispielhaft sei das an der Eigenschaft Radius in der Klasse Circle gezeigt:
| // ---------- Eigenschaft Radius in der Klasse Circle ----------
|
| public double Radius {
|
| get{return radius;}
|
| set {
|
| if(value >= 0)
|
| radius = value;
|
| else {
|
| OnMeasureError(this, out value);
|
| this.Radius = value;
|
| }
|
| }
|
| }
|
Anmerkung: Sie finden den vollständigen Code zu diesem Beispiel auf der Buch-CD unter ...\Kapitel 6\CircleApplication_5. |